﻿// <copyright file="BlockMapHeader.cs" company="Sinclair Community College">
// Copyright 2010 Sinclair Community College
// </copyright>
// <originalAuthor>Patrick S. Seymour (patrick.seymour@gmail.com)</originalAuthor>

namespace SinclairCC.AppV
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;

    /// <summary>
    /// This class represents the Block Map Header section in an App-V SFT/DSFT file.
    /// </summary>
    /// <remarks>
    /// The Block Map header contains a lookup index to the actual data blocks and
    /// thus maps a directory entry to all the data blocks associated with it. The section
    /// contains a short header and the actual block map entries, consisting of a list of
    /// blocks for each individual file entry in the directory structure. This section’s data
    /// can be compressed if the SFT file compression is in use. When compression is used,
    /// the header of the section is always in an uncompressed state and the block map entries
    /// are compressed in one datastream as a whole.
    /// </remarks>
    public class BlockMapHeader : HeaderMetadataSection
    {
        /// <summary>
        /// The case-insensitive unique identifier (GUID) for the block map.
        /// </summary>
        /// <remarks>
        /// It is generated once when the package is originally created and remains unchanged
        /// for the whole life cycle of the package.
        /// </remarks>
        private Guid id;

        // TODO: Getting different values for this field (version) than the spec says we should get.

        /// <summary>
        /// The block map version.
        /// </summary>
        /// <value>This field has the value 0x04.</value>
        private int version;

        /// <summary>
        /// The number of bits per block.
        /// </summary>
        /// <value>This field has the value 0x20.</value>
        private byte numberOfBits;

        /// <summary>
        /// The compression used in the block map data.
        /// </summary>
        /// <remarks>This field must match the SFT package's compression setting in
        /// Properties Header.</remarks>
        private CompressionMethod compressionType;

        /// <summary>
        /// This field is reserved and must have the value 0.
        /// </summary>
        /// <remarks>This field only exists if compression is used.</remarks>
        [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "This field is marked as reserved in the App-V File Format Specification.")]
        private byte[] reserved;

        /// <summary>
        /// The size in bytes of the compressed block map entries.
        /// </summary>
        private int compressedSize;

        /// <summary>
        /// The size of the block map entry section that follows the header.
        /// </summary>
        private int blockMapEntrySize;

        /// <summary>
        /// The block map entries, which follow the header.
        /// </summary>
        private List<BlockMapEntry> blockMapEntries;

        /// <summary>
        /// Initializes a new instance of the BlockMapHeader class.
        /// </summary>
        /// <param name="fileReader">
        /// A binary file reader that points to the SFT file.
        /// </param>
        public BlockMapHeader(System.IO.BinaryReader fileReader) : base(fileReader)
        {
        }

        /// <summary>
        /// Gets the unique identifier (GUID) for the block map.
        /// </summary>
        /// <remarks>
        /// This unique identifier is generated once when the package is originally created
        /// and remains unchanged for the whole life cycle of the package.
        /// </remarks>
        public Guid ID
        {
            get
            {
                this.ReadSection();
                return this.id;
            }
        }

        /// <summary>
        /// Gets the block map version.
        /// </summary>
        /// <value>This field has the value 0x04.</value>
        public int Version
        {
            get
            {
                this.ReadSection();
                return this.version;
            }
        }

        /// <summary>
        /// Gets the number of bits per block.
        /// </summary>
        /// <value>This field has the value 0x20.</value>
        public byte NumberOfBits
        {
            get
            {
                this.ReadSection();
                return this.numberOfBits;
            }
        }

        /// <summary>
        /// Gets the size in bytes of the compressed block map entries.
        /// </summary>
        public int CompressedSize
        {
            get
            {
                this.ReadSection();
                return this.compressedSize;
            }
        }

        /// <summary>
        /// Gets the size of the block map entry section that follows this header.
        /// </summary>
        public int BlockMapEntryBlockSize
        {
            get
            {
                this.ReadSection();
                return this.blockMapEntrySize;
            }
        }

        /// <summary>
        /// Gets the list of block map entries.
        /// </summary>
        public List<BlockMapEntry> BlockMapEntries
        {
            get
            {
                this.ReadSection();
                return this.blockMapEntries;
            }
        }

        /// <summary>
        /// Gets the compression used in the block map data.
        /// </summary>
        /// <remarks>
        /// This field must match the SFT package's compression setting in Properties Header.
        /// </remarks>
        public CompressionMethod CompressionType
        {
            get
            {
                this.ReadSection();
                return this.compressionType;
            }
        }

        /// <summary>
        /// Finds the block map entry for the a directory map entry.
        /// </summary>
        /// <param name="mapEntry">
        /// The directory map entry for which the block map entry is to be located.
        /// </param>
        /// <returns>
        /// Returns the block map entry for the specified directory map entry.
        /// </returns>
        public BlockMapEntry GetBlockMapEntry(DirectoryMapEntry mapEntry)
        {
            this.ReadSection();

            BlockMapEntry returnValue = this.blockMapEntries.Find(delegate(BlockMapEntry item)
            {
                return ByteArraysAreEqual(item.DirectoryEntryId, mapEntry.ID.ToByteArray(), item.DirectoryEntryId.Length);
            });

            return returnValue;
        }

        /// <summary>
        /// Reads the data for this section from the file.
        /// </summary>
        protected override void ReadSection()
        {
            if (this.SectionHasBeenRead)
            {
                return;
            }

            // Seek to the start of the data area for this section.
            long currentPosition = FileReader.BaseStream.Position; 
            FileReader.BaseStream.Seek(this.StartPositionInFile + CommonSectionHeader.HeaderSize, System.IO.SeekOrigin.Begin);

            // Read the properties from the file.
            byte[] guidBytes = FileReader.ReadBytes(16);
            this.id = new Guid(guidBytes);

            this.version = FileReader.ReadInt32();
            this.numberOfBits = FileReader.ReadByte();
            
            byte compressionFlag = FileReader.ReadByte();
            if (Enum.IsDefined(typeof(CompressionMethod), (int)compressionFlag))
            {
                this.compressionType = (CompressionMethod)compressionFlag;
            }
            else
            {
                this.compressionType = CompressionMethod.None;
            }

            if (this.compressionType == CompressionMethod.None)
            { // No compression is used.

                this.blockMapEntrySize = FileReader.ReadInt32();
                this.ReadBlockMapEntries(FileReader, this.blockMapEntrySize);
            }
            else
            {
                // Read the four bytes of reserved data.
                this.reserved = FileReader.ReadBytes(4);

                this.compressedSize = FileReader.ReadInt32();

                System.IO.BinaryReader reader = Compression.GetDecompressedReader(this.FileReader, this.compressedSize, this.compressionType);
                this.ReadBlockMapEntries(reader, reader.BaseStream.Length);
                reader.Close();
            }

            this.SectionHasBeenRead = true;
            FileReader.BaseStream.Seek(currentPosition, System.IO.SeekOrigin.Begin);
        }

        /// <summary>
        /// Determines whether two byte arrays are equal, up to the specified number of elements.
        /// </summary>
        /// <param name="left">
        /// The left array to be compared to the right array.
        /// </param>
        /// <param name="right">
        /// The right array to be compared to the left array.
        /// </param>
        /// <param name="numElements">
        /// The maximum number of elements to be compared.
        /// </param>
        /// <returns>
        /// Returns true of the specified number of elements in each array are equal.
        /// </returns>
        private static bool ByteArraysAreEqual(byte[] left, byte[] right, int numElements)
        {
            if (left.Length < numElements)
            {
                throw new ArgumentException("Left array not long enough.");
            }

            if (right.Length < numElements)
            {
                throw new ArgumentException("Right array not long enough");
            }

            for (int i = 0; i < numElements; i++)
            {
                if (left[i] != right[i])
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Reads the specified number of bytes from the given BinaryReader and processes
        /// the block map entries therein contained.
        /// </summary>
        /// <param name="reader">
        /// The BinaryReader containing the data for the block map entries.
        /// </param>
        /// <param name="numberOfBytesToRead">
        /// The number of bytes to read from the BinaryReader. This should be the length
        /// of the block map entry section.
        /// </param>
        private void ReadBlockMapEntries(System.IO.BinaryReader reader, long numberOfBytesToRead)
        {
            long blockMapStart = reader.BaseStream.Position;
            this.blockMapEntries = new List<BlockMapEntry>();
            long bytesRead = 0;
            int offsetCount = 0;
            while (bytesRead < numberOfBytesToRead)
            {
                BlockMapEntry bme = new BlockMapEntry(reader);
                this.blockMapEntries.Add(bme);
                offsetCount += bme.DataBlockCount;
                bytesRead = reader.BaseStream.Position - blockMapStart;
            }
        }
    }
}
